/// @file Simulation.cpp
/// @brief the parent class that creates all other objects for the simulation and manages interaction with the gui

#include <iostream>
#include <QString>
#include "math.h"

#include "ngl/Matrix.h"
#include "ngl/Transformation.h"
#include "ngl/NGLInit.h"
#include "ngl/Obj.h"
#include "ngl/Material.h"

#include "Simulation.h"
#include "Configuration.h"

#include "CacheItem.h"


Simulation::Simulation
                (
                   QWidget* _parent
                ) :
                QGLWidget(_parent)
{
    // set this widget to have the initial keyboard focus
    setFocusPolicy(Qt::StrongFocus);
    setFocus();

    //initialise simulation
    initialiseSimulation();
}

void Simulation::initialiseSimulation()
{
    //initialise mouse rotation values
    m_rotate=false;
    m_spinXFace=-20;
    m_spinYFace=-40;

    //initialise mouse movement values
    m_move=false;
    m_moveX=0;
    m_moveY=0;

    //init framerate and mouse move sensitivity
    ngl::Real timestep = 0;
    Configuration::initialiseWindow(m_frameRate, m_mouseMoveSensitivity, timestep, m_fpsTimerEnabled, m_cacheEnabled, m_cacheSamplingInterval, m_cacheAutomaticFlushEnabled, m_cacheAutomaticFlushInterval, m_cacheExportRBDEnabled, m_cacheExportBoundaryEnabled, m_cachePath);

    //create camera
    createCamera();

    //create light
    createLight();

    //initialise our shader library to \0null
    m_shaderLibrary = 0;

    //create the fluid solver
    m_solver = new Solver();

    //create the environment
    m_environment = new Environment();

    //create the integration
    m_integration = new Integration(SEMI_IMPLICIT_EULER, timestep);

    //create the simulation cache
    m_cache = new Cache();

    //start main timer
    m_mainSimulationTimer = startTimer(m_frameRate);

    //initialise fps timer
    m_fpsTimer = new QTimer(this);
    connect(m_fpsTimer, SIGNAL(timeout()), this, SLOT(processFPS()));
    m_fpsTimer->setInterval(1000);
    m_frameCounter = 0;
    updateFPSTimer();

    //initialise cache timer
    m_simulationCacheTimer = new QTimer(this);
    connect(m_simulationCacheTimer, SIGNAL(timeout()), this, SLOT(processSimulationCache()));
    m_simulationCacheFrameNumber = -1;

    //initialise cache autoFlush timer
    m_simulationCacheAutoFlushTimer = new QTimer(this);
    connect(m_simulationCacheAutoFlushTimer, SIGNAL(timeout()), this, SLOT(processSimulationCacheAutoFlush()));

    toggleCache();
    toggleAutomaticCacheFlush();

    //hide help by default
    m_drawHelp = false;

    //initial camera z value
    m_lastCamZ = m_camera.getEye().m_z;
}

void Simulation::deleteSimulationObjects()
{
    //kill timers
    killTimer(m_mainSimulationTimer);

    //delete all the current objects associated with the simulation
    delete m_shaderLibrary;
    delete m_solver;
    delete m_environment;
    delete m_integration;
}

Simulation::~Simulation()
{
    //clean up ngl
    std::cout << "\nNGL Cleanup" << std::endl;

    ngl::NGLInit* Init = ngl::NGLInit::instance();
    Init->NGLQuit();
    std::cout << "NGL shut down safely\n" << std::endl;

    //delete all current objects
    deleteSimulationObjects();
}

void Simulation::initializeGL()
{
    //init glew
    ngl::NGLInit* Init = ngl::NGLInit::instance();
    Init->initGlew();

    // enable depth testing for drawing
    glEnable(GL_DEPTH_TEST);

    //initialise shaders
    m_shaderLibrary = new ShaderLibrary();
    m_shaderLibrary->updateViewProjection(&m_camera);

    //initialise fluid drawing
    m_solver->initialiseDrawing();

    //initialise environment drawing
    m_environment->loadGeometry();

    //create vbo for plane grid
    ngl::VBOPrimitives *prim=ngl::VBOPrimitives::instance();
    prim->createVBOQuadPlane("plane",20,20,1,1,ngl::Vector(0,1,0));

    //create vbo for cube fluid plane [10 x 10 grid]
    prim->createVBOQuadPlane("fluidPlaneLayer", 9, 9, 9, 9, ngl::Vector(0, 1, 0));
}

void Simulation::resizeGL
                (
                    const int _w,
                    const int _h
                )
{
    //set the viewport
    glViewport(0, 0, _w, _h);

    //calculate the aspect ratio
    m_aspect = (float)_w / _h;

    //update camera aspect
    m_camera.setAspect(m_aspect);

    //update shaders ProjectionMatrix
    m_shaderLibrary->updateProjection(&m_camera);
}

void Simulation::paintGL()
{
    // clear the screen and depth buffer
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glClearColor(0.4, 0.4, 0.4, 1);

    //update tx stack with global mouse movements
    ngl::Transformation trans;
    trans.setRotation(m_spinXFace, m_spinYFace, 0);
    m_transformStack.setGlobal(trans);

    m_shaderLibrary->useShader("Light");

    //visualise fluid state
    m_solver->render(m_transformStack, m_shaderLibrary, "Light");

    //draw environment
    m_environment->draw(m_transformStack, m_shaderLibrary, "Light", "Light");

    //draw grid and reference origin
    ngl::VBOPrimitives *prim=ngl::VBOPrimitives::instance();

    //draw solver hose marker if enabled
    if (m_solver->getDrawHoseMarker())
    {
        m_transformStack.pushTransform();
        {
            m_transformStack.getCurrentTransform().setPosition(m_solver->getHoseCenter().m_x, m_solver->getHoseCenter().m_y, m_solver->getHoseCenter().m_z);
            m_transformStack.getCurrentTransform().setScale(0.3,0.3,0.3);

            //pass vertex info to shader
            m_shaderLibrary->updateModel("Light", m_transformStack.getCurrAndGlobal().getMatrix(), true);

            //pass color info to shader
            m_shaderLibrary->updateColor("Light", ngl::Colour(1, 1, 1), false);

            prim->draw("cube");
        }
        m_transformStack.popTransform();
    }

    //display stats menu
    displayHelp();
}

void Simulation::displayHelp()
{
    //display help menu
    if (m_drawHelp == true)
    {
        // first we need to fallback to the fixed functionality GL pipeline
        glUseProgram(0);

        //create font
        QFont font;
        font.setBold(false);
        font.setPointSize(12);
        glColor3f(1,1,0);

        //display debug and stats info
        QString text;
   }

}

void Simulation::timerEvent(QTimerEvent* _event)
{
    //main simulation timer
    if (_event->timerId() == m_mainSimulationTimer)
    {
        //update fluid state if fluid's auto move is enabled
        if (m_solver->getEnableAutoFluidUpdate())
        {
            //update fluid
            m_solver->updateFluid(m_environment, m_integration);

            //refresh display
            updateGL();

            //update frame counter for fps
            if (m_fpsTimerEnabled) m_frameCounter++;
        }
    }
}

void Simulation::keyPress(QKeyEvent* _event )
{
    switch (_event->key())
    {
        //debug info
//        case Qt::Key_D : { m_drawHelp ^=true; break; }

        //single step fluid update
        case Qt::Key_W : { m_solver->updateFluid(m_environment, m_integration); break; }

        //toggle timer to update fluid
        case Qt::Key_Q : { m_solver->toggleEnableAutoFluidUpdate(); updateFPSTimer(); break; }

        //inject particles in fluid solver
        case Qt::Key_A : { m_solver->injectParticles(); break; }

        //toggle to next hoseable fluid
        case Qt::Key_S : { m_solver->toggleNextHoseableFluid(); break; }

        //toggle cache timer
        case Qt::Key_C :
        {
            m_cacheEnabled = !m_cacheEnabled;
            toggleCache();

            break;
        }

        default : break;
    }

    // re-draw GL
    updateGL();
}

void Simulation::mouseMoveEvent (QMouseEvent* _event)
{
    //as the left mouse is still pressed and it moves, we get the x and y spin value
    if(m_rotate && _event->buttons() == Qt::LeftButton)
    {
        m_spinYFace = ( m_spinYFace + (_event->x() - m_origX) ) % 360 ;
        m_spinXFace = ( m_spinXFace + (_event->y() - m_origY) ) % 360 ;
        m_origX = _event->x();
        m_origY = _event->y();

        // re-draw GL
        updateGL();
    }

    //move camera in the (x,y) plane
    if(m_move && _event->buttons() == Qt::RightButton)
    {
        m_moveX = _event->x() - m_origMoveX;
        m_moveY = _event->y() - m_origMoveY;
        m_origMoveX = _event->x();
        m_origMoveY = _event->y();

        //move camera eye n look by (dx, dy)
        m_camera.move(m_moveX / m_mouseMoveSensitivity, m_moveY / m_mouseMoveSensitivity, 0);

        //update view projection matrices of camera
        m_shaderLibrary->updateViewProjection(&m_camera);

        // re-draw GL
        updateGL();
    }
}

void Simulation::mousePressEvent (QMouseEvent* _event)
{
    //keep track of origin of x,y movement for rotation
    if(_event->button() == Qt::LeftButton)
    {
        m_origX = _event->x();
        m_origY = _event->y();
        m_rotate =true;
    }

    //keep track of origin of x,y movement for tracking
    if(_event->button() == Qt::RightButton)
    {
        m_origMoveX = _event->x();
        m_origMoveY = _event->y();
        m_move =true;
    }
}

void Simulation::mouseReleaseEvent (QMouseEvent* _event)
{
    //disable rotation
    if (_event->button() == Qt::LeftButton) m_rotate=false;

    //disable tracking
    if (_event->button() == Qt::RightButton) m_move=false;

}

void Simulation::createCamera()
{
    //create camera from config parameters
    m_camera = Configuration::initialiseCamera(m_aspect);
}

void Simulation::createLight()
{
    //set a material
    ngl::Material m(ngl::PEWTER); m.use();
    m.setSpecular(ngl::Colour(1,1,1));
    m.setSpecularExponent(0.2);;

    //create our light from config and enable it
    m_light = Configuration::initialiseLight(); m_light.enable();
}

void Simulation::updateCameraZoom(int depth)
{
    //calculate the relative change in Z value
    float newZ = (float)(-depth);
    float changeInZ = newZ - m_lastCamZ;

    //calculate the small change in N that corresponds to the change in Z
    float dn = changeInZ / m_camera.getN().m_z;

    //slide the camera along the N vector by the change in N
    m_camera.slide(0, 0, dn);

    //save the new Z value
    m_lastCamZ = m_camera.getEye().m_z;

    //update view projection matrices of camera
    if ((m_shaderLibrary != NULL) && (m_shaderLibrary != 0)) m_shaderLibrary->updateViewProjection(&m_camera);

    //refresh display
    updateGL();
}

void Simulation::setup2DTopView()
{
    //2d top view enabled -> rotate our view to give a top view
    m_spinXFace=-90;
    m_spinYFace=0;

    updateGL();
}

void Simulation::setup2DFrontView()
{
    //2d front view enabled -> rotate our view to give a front view
    m_spinXFace=0;
    m_spinYFace=0;

    updateGL();
}

void Simulation::updateFPSTimer()
{
    if (m_solver->getEnableAutoFluidUpdate())
    {
        //start timer
        if (m_fpsTimerEnabled)
        {
            m_frameCounter = 0;
            m_fpsTimer->start();
        }
    }
    else
    {
        //stop timer
        m_fpsTimer->stop();
    }
}

void Simulation::processFPS()
{
    std::cout << "FPS : " << m_frameCounter << "\n";
    m_frameCounter = 0;
}

void Simulation::toggleCache()
{
    m_simulationCacheTimer->stop();

    if (m_cacheEnabled)
    {
        //cache current initial frame
        m_simulationCacheTimer->singleShot(0, this, SLOT(processSimulationCache()));

        //start timers
        m_simulationCacheTimer->start(m_cacheSamplingInterval);
    }
}

void Simulation::toggleAutomaticCacheFlush()
{
    //stop timers
    m_simulationCacheAutoFlushTimer->stop();

    if (m_cacheAutomaticFlushEnabled)
    {
        //start timers
        m_simulationCacheAutoFlushTimer->start(m_cacheAutomaticFlushInterval);
    }
}

void Simulation::processSimulationCache()
{
    if (m_cacheEnabled)
    {
        ++m_simulationCacheFrameNumber;

        //create a fluid cache item and add it to the cache
        m_cache->addItem(CacheItem(m_solver->getPositionList(), m_solver->getNameList(), m_simulationCacheFrameNumber, FLUID_CACHE));

        //create a boundary cache item and add it to the cache if enabled
        if (m_cacheExportBoundaryEnabled) m_cache->addItem(CacheItem(m_environment->getBoundaryPositionList(), m_environment->getBoundaryNameList(), m_simulationCacheFrameNumber, BOUNDARY_CACHE));

        //create a rbd cache item and add it to the cache if enabled
        if (m_cacheExportRBDEnabled && m_environment->getObstacleEnabled())
        {
            m_cache->addItem(CacheItem(m_environment->getSpherePositionList(), m_environment->getSphereNameList(), m_simulationCacheFrameNumber, SPHERE_CACHE));
            m_cache->addItem(CacheItem(m_environment->getCapsulePositionList(), m_environment->getCapsuleNameList(), m_simulationCacheFrameNumber, CAPSULE_CACHE));
        }

        std::cout << "Cached frame number : " << m_simulationCacheFrameNumber << "\n";
    }
}

void Simulation::processSimulationCacheAutoFlush()
{
    if (m_cacheAutomaticFlushEnabled) flushCache();
}

void Simulation::flushCache()
{
    //flush cache to file
    m_cache->flushCache(m_cachePath);

    std::cout << "Cache Flushed\n";
}


